跳到主要内容

Lambda 函数式接口

函数式编程思想

在数学中,函数就是有输入量、输出量的一套计算方案,也就是"拿什么东西做什么事" 相对而言,面向对象过分强调"必须通过对象的形式来做事情",而函数式思想则尽量忽略面向对象的复杂语法 强调:做什么,而不是以什么形式去做

面向对象的思想: 做一件事,找一个能解决这个事情的对象,调用对象的方法,完成事情。

函数式编程的思想: 只要能获取到结果,谁去做的,怎么做的都不重要,重使的是结果,不重视过程

而在 Java 中的函数式编程思想的体现就是 Lambda,所以函数式接口就是可以适用于 Lambda 使用的接口。只有确保接口中有且仅有一个抽象方法,Java 中的 Lambda 才能顺利的推导

什么是函数式接口

函数式接口在 Java 中是指:有且仅有一个抽象方法的接口 (像默认方法之类的还是可以有的)

Lambda 是靠接口来进行推断的,所以得确保有且仅有一个抽象方法

// 这个 @FunctionalInterface 和 @Override 一样,不强制要求
@FunctionalInterface
public interface MyInfFun {
public String say(String name);
}

有了这个 @FunctionalInterface Lambda 就能直接推导了

@Test
public void testSayHello() throws Exception {
MyInfFun myInfFun = (name) -> name + "Hello";
System.out.println(myInfFun.say("小美"));
}

匿名类 与 Lambda 区别

参考资料 Java 中的匿名类与 Lambda表达式

匿名类Lambda表达式
没有名字的类没有名称的方法(匿名函数)
它可以扩展抽象或具体的类它不能扩展抽象或具体的类
它可以实现包含任何抽象方法的接口它可以实现一个包含单个抽象方法的接口
匿名类可以具有实例变量和方法局部变量Lambda表达式只能具有局部变量
可以实例化匿名内部类Lambda 表达式无法实例化
在匿名内部类内部,“ this”始终是指当前匿名内部类对象,而不是外部对象在Lambda表达式内部,“this” 始终引用当前的外部类对象,即包围类对象
如果我们要处理多种方法,这是最佳选择如果我们要处理接口,这是最佳选择
在编译时,将生成一个单独的.class文件在编译时,不会生成单独的 .class 文件。只是将其转换为外部类的私有方法
每当我们创建对象时,内存分配都是按需的它驻留在JVM的永久内存中

Lambda 的 this 指针

参考资料 Java 匿名类中this和Lambda表达式中this的区别 参考资料 java8里 lambda 里的 this 为什么会指向 Lambda 所在的外部类

今天写代码时发现在 Lambda 中使用 this 指针报错了

爆的是参数类型不对,这就很奇怪了,因为按说 Lambda 不就是语法糖吗,本质还应该是匿名函数吧

事实上匿名类中的 this 是 “匿名类对象”本身;Lambda 表达式中的 this 是 “调用Lambda表达式的对象”。

写了一个例子,作为测试:

import java.util.function.Supplier;

public class LambdaTest {
public static void main(String[] args) {
new LambdaTest().test();
}

public void test() {
String para = "abc";
String para2 = "abc";
System.out.println(this);
Supplier<String> supplier = () -> {
return para + para2;
};
System.out.println(supplier.get());
}
}

对应的 jvm 机器码是:

  private static java.lang.String lambda$test$0(java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: new #12 // class java/lang/StringBuilder
3: dup
4: invokespecial #13 // Method java/lang/StringBuilder."<init>":()V
7: aload_0
8: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
11: aload_1
12: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokevirtual #15 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
18: areturn
LineNumberTable:
line 16: 0

通过方法签名可以知道,如果一个类没有带 this,被编译成了一个静态内部类方法。

但是使用了 this 之后

import java.util.function.Supplier;

public class LambdaTest {
public static void main(String[] args) {
new LambdaTest().test();
}

public void test() {
String para = "abc";
String para2 = "abc";
System.out.println(this);
Supplier<String> supplier = () -> {
System.out.println(this);
return para + para2;
};
System.out.println(supplier.get());
}
}

对应的 jvm 机器码:

  private java.lang.String lambda$test$0(java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PRIVATE, ACC_SYNTHETIC
Code:
stack=2, locals=3, args_size=3
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
7: new #12 // class java/lang/StringBuilder
10: dup
11: invokespecial #13 // Method java/lang/StringBuilder."<init>":()V
14: aload_1
15: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: aload_2
19: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #15 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: areturn
LineNumberTable:
line 15: 0
line 16: 7

lambda 被编译成了一种内部类!

结论:

lambda 一般情况下会被编译成静态匿名方法,引用的外部变量以参数的方式传递。 如果 lambda 里使用了this 指标,则被编译为匿名内部方法,以让 this 指针指向lambda 外部类。

函数式编程使用场景

Lambda的延迟执行

日志案例(性能浪费)

如下的日志就存在性能浪费,因为只有当 level 为 1 时才执行这个消息打印,而当 level 不是 1 时,这个拼接字符串的操作就浪费掉了

public class Demo01Logger {
public static void showLog(int level,String message){
if (level == 1) {
System.out.println(message);
}
}

public static void main(String[] args) {
String mg1 = "hello";
String mg2 = "world";
String mg3 = "java";

showLog(2, mg1 + mg2 + mg3);
}
}

日志案例使用 Lambda 改进

改用接口来传递一个函数式接口,只有当满足条件才执行字符拼接

// 先创建一个函数式接口  @FunctionalInterface不加也不会报错,这玩意就是给人看的
@FunctionalInterface
public interface MessageBuilder {
String builderMessage();
}
public class Demo01Logger {
public static void showLog(int level,MessageBuilder message){
if (level == 1) {
System.out.println(message.builderMessage());
}
}

public static void main(String[] args) {
String mg1 = "hello";
String mg2 = "world";
String mg3 = "java";

showLog(1,() -> mg1 + mg2 + mg3);
}
}

下面介绍的接口的例子都是使用的延时调用

常用的函数式接口

生产型接口 Supplier

Supplier<T> 接口仅包含一个无参的方法:T get() 用来获取一个泛型参数指定类型的对象数据

Supplier<T> 接口被称为 生产型接口,指定接口的泛型是什么类型。那么接口中的 get 方法就会产生什么类型的数据

例:求数组元素最大值

使用 Supplier 接口作为方法参数类型,通过 Lambda 表达式求出 int 数组中的最大值(要用包装类)

public class Demo {
public static Integer getMax(Supplier<Integer> supplier) {
return supplier.get();
}

public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 2, 3, 7, 1, 2, -20, 188, 26, 99);

System.out.println("最大值为:" + getMax(() -> {
Integer max = list.get(0);

for (int i : list) {
if (i > max)
max = i;
}
return max;
}));
}
}

消费型接口 Consumer

Consumer<T> 接口正好与 Supplier 接口相反,它不是生产一个数据,而是消费一个数据,其类型由泛型决定,传入一个指定类型的参数,使用 accept 对其进行消费

public class Demo {
/*
定义一个方法
方法的参数传递一个字符串的姓名
方法的参数传递Consumer接口,泛型使用String
可以使用Consumer接口消费字符串的姓名
*/
public static void method(String name, Consumer<String> con){
con.accept(name);
}

public static void main(String[] args) {
method("张三", (String name) -> {
// 对传递的字符串进行消费
name += "说:你好";
System.out.println(name);
});
}
}

Consumer<T> 接口的 andThen 方法可以把两个 Consumer 组合到一起,再对数据消费(需要两个 Consumer 参数)

// 例子:对字符串切割分别执行
public class Demo {
public static void printInfo(String[] arr, Consumer<String> con1, Consumer<String> con2) {
// 遍历打印
for (String message : arr) {
// 使用 andThan 方法连接两个 Consumer 接口
// 谁写前面谁先消费
con1.andThen(con2).accept(message);
}
}

public static void main(String[] args) {
String[] arr = {"张三,男", "小美,女", "小红,女"};
printInfo(arr,
message -> {
String name = message.split(",")[0];
System.out.print("名字:" + name);
},
message -> {
String sex = message.split(",")[1];
System.out.println(" sex:" + sex);
});
}
}

// 输出为
// 名字:张三 sex:男
// 名字:小美 sex:女
// 名字:小红 sex:女

断言型接口 Predicate

Predicate<T> 用于需要对某种类型的数据进行判断时得到一个 Boolean 值结果,使用这个 test() 方法进行调用

public class Demo {
public static boolean checkString(String s, Predicate<String> pre) {
return pre.test(s);
}

public static void main(String[] args) {
// 判断字符串是否大于5
boolean b = checkString("hello world", str -> str.length() > 5);
System.out.println(b);
}
}

既然是条件判断,就会存在与、或、非三种常见的逻辑关系,所以可以像上面的消费型那样使用 andor 方法将多个参数连接起来判断

使用例:信息筛选

public class Demo {
public static List<String> filter(String[] arr, Predicate<String> one, Predicate<String> two) {
List<String> list = new ArrayList<>();
for (String s : arr) {
if (one.and(two).test(s)) {
list.add(s);
}
}
return list;
}

public static void main(String[] args) {
String[] arr = {"小明,男", "静香,女", "小红,女"};
List<String> list = filter(arr,
item -> item.split(",")[1].equals("女"),
item -> item.split(",")[0].contains("小"));
list.forEach(System.out::println);
}
}

单参接口

Function<T,R> 这个接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件,它使用 apply() 方法接受一个输入参数T,返回一个结果R

使用场景例:将 String 类型转换为 Integer 类型

public class Demo {
public static void change(String s, Function<String,Integer> fun) {
int in = fun.apply(s);
System.out.println(in);
}

public static void main(String[] args) {
change("helloWorld", String::length);
}
}

注意:这里的 String::length 双冒号运算符(方法引用)等价于

(String str) -> {
return str.length();
}

// 只不过这个 String 里面已经存在了这个 length() 方法了,所以直接引用它的方法来取得返回值

同上面那个 Consumer<T>andThen 方法一样,这个 Function<T,R> 也有一个 andThen 方法用来连接两个参数

使用例:把 String 类型的 123,转换为 Integer 类型,把转换后的结果加10,再把这个 Integer 类型转回 String 类型

public class Demo {
public static void change(String s, Function<String,Integer> fun1,Function<Integer,String> fun2) {
String str = fun1.andThen(fun2).apply(s);
System.out.println(str);
}

public static void main(String[] args) {
change("133", str->Integer.parseInt(str)+100, String::valueOf);
}
}

注意:这里的 String::valueOf 双冒号运算符等价于

(Int)->String.valueOf(Int)

两个参数接口

BiFunction<T, U, R> 也是一个函数式接口,和 Function 接口不同的是,它在接口中声明了3个泛型,其中前两个作为方法参数类型,最后一个作为返回类型

@Test
public void test() {
BiFunction<String, String, String> f1 = (x,y) -> "world " + x + y;
Function<String, String> f2 = (x) -> "hello " + x;

System.out.println(f1.apply("zhang", "san"));
System.out.println(f1.andThen(f2).apply("li", "si"));
}

双冒号(::)方法引用

英文:double colon,双冒号(::)运算符在 Java 8 中被用作 方法引用(method reference),方法引用是与 lambda 表达式相关的一个重要特性,它的原理就是直接调用实例的某个方法

如下实例:

public String extractUsername(String token) {
// 这里直接引用 Claims 类里面的 getSubject 方法
return extractClaim(token, Claims::getSubject);
}

// 它等价于下面这个
// public String extractUsername(String token) {
// return extractClaim(token, (Claims claims)-> {
// return claims.getSubject();
// });
// }

public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}

// 这个 Function 表示一个接受一个参数并产生结果的函数。
// <T> 函数输入的类型(就是 apply 方法的参数类型)
// <R> 函数结果的类型(就是 apply 方法的返回值)
public <R> R extractClaim(String token, Function<Claims, R> claimsResolver) {
final Claims claims = extractAllClaims(token);
// 这个 Function 函数接口通过调用 apply 取得结果
return claimsResolver.apply(claims);
}

总之把它理解成函数指针就行了

构造器引用

private Person constructorRef(Supplier<Person> sup){
return sup.get();
}

@Test
public void testConstructorRef(){
Person p = constructorRef(Person::new);
System.out.println(p);
}

// 等价于

@Test
public void testConstructorRef(){
Person p = constructorRef(() -> {
return new Person();
});
System.out.println(p);
}

需要有无参的构造器。

静态方法引用

private static void print(String s){
System.out.println(s);
}

@Test
public void testStaticRef(){
Arrays.asList("aa","bb","cc").forEach(TestMethodReference::print);
}

// 等价于

@Test
public void testStaticRef(){
Arrays.asList("aa","bb","cc").forEach((String item)-> {
TestMethodReference.print(item);
});
}

只要静态方法的参数列表和 FI 需要的参数一致就可以。

成员方法引用

public String extractUsername(String token) {
// 这里直接引用 Claims 类里面的 getSubject 方法
return extractClaim(token, Claims::getSubject);
}

// 它等价于下面这个
// public String extractUsername(String token) {
// return extractClaim(token, (Claims claims)-> {
// return claims.getSubject();
// });
// }

public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}

// 这个 Function 表示一个接受一个参数并产生结果的函数。
// <T> 函数输入的类型(就是 apply 方法的参数类型)
// <R> 函数结果的类型(就是 apply 方法的返回值)
public <R> R extractClaim(String token, Function<Claims, R> claimsResolver) {
final Claims claims = extractAllClaims(token);
// 这个 Function 函数接口通过调用 apply 取得结果
return claimsResolver.apply(claims);
}

高阶函数的用法

高阶函数就是返回函数的函数

级联表达式

Function<Integer, Function<Integer, Integer>> fun = x -> y -> x + y;
System.out.println(fun.apply(2).apply(3)); // 输出结果为 5

柯里化

紧接上面的级联表达式

柯里化是什么呢?它主要用于标准化输入,例如下面这样使用

Function<Integer, Function<Integer, Function<Integer, Integer>>> fun2 =
x -> y -> z -> x + y + z;
int[] nums = {2, 3, 4};

Function f = fun2; // 这里有点像链表遍历用的那个指针
// 这个循环等价于:fun.apply(2).apply(3).apply(4)
for (int num : nums) {
if (fun2 instanceof Function) {
Object obj = f.apply(num);
if (obj instanceof Function) {
f = (Function) obj;
} else {
System.out.println("调用结束,最终结果为:" + obj);
}
}
}

可以看到上面柯里化的好处,因为标准化了输入,所以可以动态的改变 fun2 的长度,可以级联 2 次,也可以级联 3 次、4次,最终因为都是标准的只传入一个参数,所以不管级联多少次,都可以通过 for 循环这样调用得到结果